#coding:utf-8
import tkinter as tk
from tkinter import scrolledtext, ttk, filedialog, messagebox
import requests
import re
import threading
from queue import Queue
import time
import urllib3
from PIL import Image, ImageTk
import io

# 禁用SSL警告
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

class SpringDetector:
    def __init__(self, root):
        self.root = root
        self.root.title("信安西部-明镜高悬实验室-Spring&SpringBoot批量鉴别工具（v1.0）")
        self.root.geometry("900x700")
        self.root.resizable(True, True)
        
        # 确保中文显示正常
        self.font = ('SimHei', 10)
        
        # 检测队列和线程控制
        self.url_queue = Queue()
        self.total_urls = 0
        self.processed = 0
        self.running = False
        
        # Spring Boot默认favicon的特征值（简化版）
        self.spring_boot_favicon_hash = "84b761b5268a411415a7ca776573890d"
        
        self.create_widgets()
        
    def create_widgets(self):
        # 顶部控制区域
        control_frame = ttk.Frame(self.root, padding="10")
        control_frame.pack(fill=tk.X, expand=False)
        
        # URL输入区域
        input_frame = ttk.LabelFrame(self.root, text="URL列表", padding="10")
        input_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
        
        self.url_text = scrolledtext.ScrolledText(input_frame, wrap=tk.WORD, font=self.font, height=10)
        self.url_text.pack(fill=tk.BOTH, expand=True)
        self.url_text.insert(tk.END, "http://example1.com\nhttps://example2.com\n")
        
        # 按钮区域
        btn_frame = ttk.Frame(self.root, padding="10")
        btn_frame.pack(fill=tk.X, expand=False)
        
        ttk.Button(btn_frame, text="加载URL文件", command=self.load_url_file).pack(side=tk.LEFT, padx=5)
        ttk.Button(btn_frame, text="清空URL", command=self.clear_urls).pack(side=tk.LEFT, padx=5)
        ttk.Button(btn_frame, text="开始检测", command=self.start_detection).pack(side=tk.LEFT, padx=5)
        ttk.Button(btn_frame, text="停止检测", command=self.stop_detection).pack(side=tk.LEFT, padx=5)
        ttk.Button(btn_frame, text="导出结果", command=self.export_results).pack(side=tk.RIGHT, padx=5)
        
        # 选项区域
        option_frame = ttk.LabelFrame(self.root, text="检测选项", padding="10")
        option_frame.pack(fill=tk.X, expand=False, padx=10, pady=5)
        
        ttk.Label(option_frame, text="超时时间(秒):", font=self.font).grid(row=0, column=0, padx=5, pady=5)
        self.timeout_var = tk.StringVar(value="10")
        ttk.Entry(option_frame, textvariable=self.timeout_var, width=10).grid(row=0, column=1, padx=5, pady=5)
        
        ttk.Label(option_frame, text="线程数:", font=self.font).grid(row=0, column=2, padx=5, pady=5)
        self.thread_var = tk.StringVar(value="5")
        ttk.Entry(option_frame, textvariable=self.thread_var, width=10).grid(row=0, column=3, padx=5, pady=5)
        
        # 结果显示区域
        result_frame = ttk.LabelFrame(self.root, text="检测结果", padding="10")
        result_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
        
        # 结果表格
        columns = ("url", "status", "type", "info")
        self.result_tree = ttk.Treeview(result_frame, columns=columns, show="headings")
        
        # 设置列宽和标题
        self.result_tree.column("url", width=300, anchor=tk.W)
        self.result_tree.column("status", width=80, anchor=tk.CENTER)
        self.result_tree.column("type", width=120, anchor=tk.CENTER)
        self.result_tree.column("info", width=300, anchor=tk.W)
        
        self.result_tree.heading("url", text="URL")
        self.result_tree.heading("status", text="状态")
        self.result_tree.heading("type", text="类型")
        self.result_tree.heading("info", text="信息")
        
        # 添加滚动条
        scrollbar = ttk.Scrollbar(result_frame, orient=tk.VERTICAL, command=self.result_tree.yview)
        self.result_tree.configure(yscroll=scrollbar.set)
        
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.result_tree.pack(fill=tk.BOTH, expand=True)
        
        # 状态栏
        self.status_var = tk.StringVar(value="就绪")
        status_bar = ttk.Label(self.root, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W)
        status_bar.pack(side=tk.BOTTOM, fill=tk.X)
        
    def load_url_file(self):
        """加载包含URL的文本文件"""
        file_path = filedialog.askopenfilename(
            filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")]
        )
        if file_path:
            try:
                with open(file_path, 'r', encoding='utf-8') as f:
                    content = f.read()
                    self.url_text.delete(1.0, tk.END)
                    self.url_text.insert(tk.END, content)
                messagebox.showinfo("成功", f"已加载文件: {file_path}")
            except Exception as e:
                messagebox.showerror("错误", f"加载文件失败: {str(e)}")
    
    def clear_urls(self):
        """清空URL输入区域"""
        self.url_text.delete(1.0, tk.END)
    
    def start_detection(self):
        """开始批量检测"""
        # 清空之前的结果
        for item in self.result_tree.get_children():
            self.result_tree.delete(item)
        
        # 获取URL列表
        url_text = self.url_text.get(1.0, tk.END).strip()
        if not url_text:
            messagebox.showwarning("警告", "请输入URL列表")
            return
        
        urls = [url.strip() for url in url_text.split('\n') if url.strip()]
        if not urls:
            messagebox.showwarning("警告", "未找到有效的URL")
            return
        
        # 获取配置参数
        try:
            timeout = int(self.timeout_var.get())
            thread_count = int(self.thread_var.get())
            if thread_count < 1 or thread_count > 50:
                raise ValueError("线程数必须在1-50之间")
            if timeout < 1 or timeout > 60:
                raise ValueError("超时时间必须在1-60之间")
        except ValueError as e:
            messagebox.showerror("参数错误", str(e))
            return
        
        # 初始化队列
        self.url_queue = Queue()
        for url in urls:
            self.url_queue.put(url)
        
        self.total_urls = len(urls)
        self.processed = 0
        self.running = True
        
        # 更新状态
        self.status_var.set(f"准备开始检测，共{self.total_urls}个URL")
        
        # 启动检测线程
        for _ in range(thread_count):
            threading.Thread(target=self.detection_worker, args=(timeout,), daemon=True).start()
        
        # 启动进度更新线程
        threading.Thread(target=self.update_progress, daemon=True).start()
    
    def stop_detection(self):
        """停止检测"""
        self.running = False
        self.status_var.set("已停止检测")
    
    def detection_worker(self, timeout):
        """检测工作线程"""
        while self.running and not self.url_queue.empty():
            url = self.url_queue.get()
            try:
                result = self.detect_spring(url, timeout)
                self.root.after(0, self.add_result, url, result)
            except Exception as e:
                self.root.after(0, self.add_result, url, {
                    'status': '错误',
                    'type': 'N/A',
                    'info': str(e)
                })
            finally:
                self.processed += 1
                self.url_queue.task_done()
    
    def update_progress(self):
        """更新进度状态"""
        while self.running and (self.processed < self.total_urls):
            self.status_var.set(f"正在检测: {self.processed}/{self.total_urls}")
            time.sleep(0.5)
        if self.running:
            self.status_var.set(f"检测完成: {self.processed}/{self.total_urls}")
            self.running = False
    
    def add_result(self, url, result):
        """添加检测结果到表格"""
        # 设置行颜色
        tag = "springboot" if result['type'] == "Spring Boot" else \
              "spring" if result['type'] == "Spring" else \
              "error" if result['status'] == "错误" else "other"
        
        self.result_tree.insert("", tk.END, values=(
            url,
            result['status'],
            result['type'],
            result['info']
        ), tags=(tag,))
        
        # 配置标签样式
        self.result_tree.tag_configure("springboot", background="#e1f5fe")
        self.result_tree.tag_configure("spring", background="#e8f5e9")
        self.result_tree.tag_configure("error", background="#ffebee")
    
    def detect_spring(self, url, timeout):
        """检测URL是否为Spring或Spring Boot应用"""
        # 标准化URL
        if not url.startswith(('http://', 'https://')):
            url = f"http://{url}"
        
        # 整合的检测特征
        # 1. Spring框架特征
        spring_headers = {
            'X-Powered-By': 'Spring'
        }
        
        spring_paths = [
            '/spring', 
            '/mvc',
            '/spring-mvc',
            '/beans',            # Spring MVC端点
            '/env',              # Spring环境变量端点
        ]
        
        # 2. Spring Boot特征
        spring_boot_headers = [
            'X-Application-Context',
            'X-Spring-Cloud-Context',
            'X-Spring-Boot-Version',
            'Server'  # 用于检测Tomcat/Jetty等内嵌服务器
        ]
        
        spring_boot_paths = [
            '/actuator',         # Spring Boot Actuator
            '/actuator/health',  # 健康检查端点
            '/error',            # 默认错误页面
        ]
        
        # 3. 错误页面特征
        spring_error_patterns = [
            'org.springframework',
            'Spring MVC'
        ]
        
        spring_boot_error_patterns = [
            'Whitelabel Error Page',
            'This application has no explicit mapping for /error'
        ]
        
        try:
            # 检测主页响应
            main_response = requests.get(url, timeout=timeout, verify=False, allow_redirects=True)
            
            # 检查响应头中的Spring特征
            for header, value in spring_headers.items():
                if header in main_response.headers and value in main_response.headers[header]:
                    return {
                        'status': '存活',
                        'type': 'Spring',
                        'info': f"检测到{header}: {value}响应头"
                    }
            
            # 检查响应头中的Spring Boot特征
            for header in spring_boot_headers:
                if header in main_response.headers:
                    header_value = main_response.headers[header]
                    if header == 'Server' and re.search(r'Tomcat|Jetty', header_value):
                        return {
                            'status': '存活',
                            'type': 'Spring Boot',
                            'info': f"检测到内嵌服务器: {header_value}"
                        }
                    else:
                        return {
                            'status': '存活',
                            'type': 'Spring Boot',
                            'info': f"检测到{header}响应头"
                        }
            
            # 检查响应内容中的错误页面特征
            for pattern in spring_boot_error_patterns:
                if pattern in main_response.text:
                    return {
                        'status': '存活',
                        'type': 'Spring Boot',
                        'info': f"检测到Spring Boot错误页面特征: {pattern}"
                    }
            
            for pattern in spring_error_patterns:
                if re.search(pattern, main_response.text, re.IGNORECASE):
                    return {
                        'status': '存活',
                        'type': 'Spring',
                        'info': f"响应内容包含{pattern}"
                    }
            
            # 检测favicon.ico
            try:
                favicon_url = url.rstrip('/') + '/favicon.ico'
                favicon_response = requests.get(favicon_url, timeout=timeout, verify=False, allow_redirects=True)
                if favicon_response.status_code == 200 and 'image' in favicon_response.headers.get('Content-Type', ''):
                    # 简单检测Spring Boot默认图标特征
                    if self.check_spring_boot_favicon(favicon_response.content):
                        return {
                            'status': '存活',
                            'type': 'Spring Boot',
                            'info': "检测到Spring Boot默认图标"
                        }
            except:
                pass  # 不强制要求favicon存在
            
            # 检测Spring特定路径
            for path in spring_paths:
                try:
                    path_response = requests.get(f"{url}{path}", timeout=timeout, verify=False, allow_redirects=True)
                    if path_response.status_code in [200, 401, 403]:  # 200成功，401/403表示存在但无权限
                        for pattern in spring_error_patterns:
                            if re.search(pattern, path_response.text, re.IGNORECASE):
                                return {
                                    'status': '存活',
                                    'type': 'Spring',
                                    'info': f"{path}路径包含{pattern}"
                                }
                        return {
                            'status': '存活',
                            'type': 'Spring',
                            'info': f"检测到Spring特征路径: {path}"
                        }
                except:
                    continue
            
            # 检测Spring Boot特定路径
            for path in spring_boot_paths:
                try:
                    path_response = requests.get(f"{url}{path}", timeout=timeout, verify=False, allow_redirects=True)
                    if path_response.status_code in [200, 401, 403]:  # 200成功，401/403表示存在但无权限
                        return {
                            'status': '存活',
                            'type': 'Spring Boot',
                            'info': f"检测到Spring Boot特征路径: {path}"
                        }
                except:
                    continue
            
            # 未检测到明显特征
            return {
                'status': '存活',
                'type': '未知',
                'info': "未检测到Spring相关特征"
            }
            
        except requests.exceptions.RequestException as e:
            return {
                'status': '不可达',
                'type': 'N/A',
                'info': f"请求失败: {str(e)}"
            }
        except Exception as e:
            return {
                'status': '错误',
                'type': 'N/A',
                'info': f"处理错误: {str(e)}"
            }
    
    def check_spring_boot_favicon(self, image_content):
        """简单检测是否为Spring Boot默认图标"""
        try:
            # 计算简单的哈希值进行比对（实际应用中可使用更精确的方法）
            import hashlib
            hash_value = hashlib.md5(image_content).hexdigest()
            return hash_value == self.spring_boot_favicon_hash
        except:
            return False
    
    def export_results(self):
        """导出检测结果"""
        if not self.result_tree.get_children():
            messagebox.showwarning("警告", "没有结果可导出")
            return
        
        file_path = filedialog.asksaveasfilename(
            defaultextension=".txt",
            filetypes=[("文本文件", "*.txt"), ("CSV文件", "*.csv"), ("所有文件", "*.*")]
        )
        
        if file_path:
            try:
                with open(file_path, 'w', encoding='utf-8') as f:
                    # 写入表头
                    f.write("URL,状态,类型,信息\n")
                    
                    # 写入内容
                    for item in self.result_tree.get_children():
                        values = self.result_tree.item(item, "values")
                        f.write(f"{values[0]},{values[1]},{values[2]},{values[3]}\n")
                
                messagebox.showinfo("成功", f"结果已导出至: {file_path}")
            except Exception as e:
                messagebox.showerror("错误", f"导出失败: {str(e)}")

if __name__ == "__main__":
    root = tk.Tk()
    app = SpringDetector(root)
    root.mainloop()
